/*
 * KeyValueCodingSet.java
 *
 * Created on November 7, 2006, 12:03 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.framework;

import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 *
 * @author grant
 */
public class KeyValueCodingSet extends KeyValueCodingBase implements Set
{
    /** 
     * Key that notifications are sent on when the set changes.  You can 
     * register directly on a set for this key, if you like, or you can register
     * with a ManagedData object that contains the set instead, on the key that
     * contains the set.  The ManagedData object listens for these modifications
     * and will forward the notifications on the proper key.
     * Note that this value that you can get/set for this key is meaningless.
     */
    public final static String SET_CHANGE_NOTIFICATION_KEY = ".";
    
    /** The actual set. */
    private Set set;
    
    /** Creates a new instance of KeyValueCodingSet */
    public KeyValueCodingSet()
    {
        this(new HashSet());
    }
    
    /**
     * Creates a new instance of KeyValueCodingSet that uses the given set.
     * This set is used
     * to implement underlying HashSet operations, so it is important that the
     * set not be modified after being used to construct the ManagedSet.
     * @param set A set to use to perform set operations.  The set is used directly
     * without copying.  Do not modify set after using it to construct a
     * ManagedSet or use it to create other ManagedSets.
     */
    public KeyValueCodingSet(Set set)
    {
        this.set = set;
    }
    
    /**
     * Returns an iterator that can iterate over the items in the set.  It 
     * additionally sends notifications when the set changes due to a call
     * to remove.
     */
    public Iterator iterator()
    {
        return new Iterator()
        {
            private Iterator underlyingIterator = set.iterator();
            private Object lastReturned = null;
            
            public boolean hasNext()
            {
                return underlyingIterator.hasNext();
            }
            public Object next()
            {
                lastReturned = underlyingIterator.next();
                return lastReturned;
            }
            public void remove()
            {
                if (lastReturned != null)
                {
                    SingleSet modifyingSet = new SingleSet(lastReturned);
                    willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.MINUS, modifyingSet);
                    underlyingIterator.remove();
                    lastReturned = null;
                    didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.MINUS, modifyingSet);
                } else {
                    // this will throw an IllegalStateException
                    underlyingIterator.remove();
                }
            }
        };
    }

    /** 
     * Get the number of items in the set.
     * @return The number of items in the set.
     */
    public int size()
    {
        return set.size();
    }

    /**
     * Test the set for emptiness.
     * @return True if the set is empty (has no objects in it) false otherwise
     * (if it has one or more objects in it)
     */
    public boolean isEmpty()
    {
        return set.isEmpty();
    }

    /**
     * Test to see if the set contains an object.
     * @param o The object to test to see if it is in the set.
     * @return True if the set contains the object, false otherwise.
     */
    public boolean contains(Object o)
    {
        return set.contains(o);
    }

    /**
     * Create and return an array containing all of the objects in the set.
     * @return A newly-allocated array containing all of the items in the set.
     */
    public Object[] toArray()
    {
        return set.toArray();
    }

    /**
     * Return an array containing all of the objects of the set.
     * @param t An array to use.  If it is large enough to hold all the items
     * in the set, the items in the set are stored in it.
     * @return t if it was large enough to hold all the items in the set, or
     * a newly-allocated array of the same type as t if it was not.
     */
    public Object[] toArray(Object[] t)
    {
        return set.toArray(t);
    }
    
    
    /**
     * Adds a single object to this ManagedSet.  Sends a notification of type
     * UNION.
     * @param o The object to add to the set.
     */
    public boolean add(Object o)
    {
        return addAll(new SingleSet(o));
    }

    /**
     * Removes a single object from this ManagedSet.  Sends a notification of
     * type MINUS.
     * @param o The object to remove.
     */
    public boolean remove(Object o)
    {
        return removeAll(new SingleSet(o));
    }

    public boolean containsAll(Collection collection)
    {
        return set.containsAll(collection);
    }
    
    /**
     * Adds all of the items in the collection to this set, and sends a 
     * notification of type UNION.
     * @param collection The collection to add items of to this set.
     */
    public boolean addAll(Collection collection)
    {
        final Set modifyingSet = new HashSet(collection);
        modifyingSet.removeAll(set);
        if (!modifyingSet.isEmpty())
        {
            willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.UNION, modifyingSet);
            set.addAll(modifyingSet);
            didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.UNION, modifyingSet);
            return true;
        }
        return false;
    }
    /**
     * Retains all the items in the set that are also in the collection, and 
     * sends a notification of type MINUS with the items that were removed.
     * @param collection The collection containing items to intersect with the
     * items in this set.
     */
    public boolean retainAll(Collection collection)
    {
        final Set itemsToReplace = new HashSet(set);
        itemsToReplace.removeAll(collection);
        if (!itemsToReplace.isEmpty())
        {
            willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.MINUS, itemsToReplace);
            set.removeAll(itemsToReplace);
            didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.INTERSECT, itemsToReplace);
            return true;
        }
        return false;
    }

    /**
     * Removes everything in the set that is in the collection, and sends a 
     * notification of type MINUS.
     * @param collection The collection of objects to remove.
     */
    public boolean removeAll(Collection collection)
    {
        final Set itemsToReplace = new HashSet(collection);
        itemsToReplace.retainAll(set);
        if (!itemsToReplace.isEmpty())
        {
            willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.MINUS, itemsToReplace);
            set.removeAll(collection);
            didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.MINUS, itemsToReplace);
            return true;
        }
        return false;
    }

    /**
     * Empties the set, and sends a notification of type REPLACE
     */
    public void clear()
    {
        if (!set.isEmpty())
        {
            willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.REPLACE, EMPTY_SET);
            set.clear();
            didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.REPLACE, EMPTY_SET);
        }
    }
    
    /**
     * Replaces the current contents of the set with another set.
     * Sends a notification of type REPLACE
     * @param replacementCollection A collection of objects to add to the set 
     * after removing everything that is in the set now.
     */
    public void replace(Collection replacementCollection)
    {
        Set otherSet;
        if (replacementCollection instanceof Set)
            otherSet = (Set) replacementCollection;
        else
            otherSet = new HashSet(replacementCollection);
        willChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.REPLACE, otherSet);
        set.clear();
        set.addAll(otherSet);
        didChangeValueForKey(SET_CHANGE_NOTIFICATION_KEY, KeyValueObserver.SetMutationType.REPLACE, otherSet);
    }

    
    /**
     * Called BEFORE the set is mutated.  It will be paired with an identical
     * call to didChangeValueForKey();
     * @param key The key that's about to be changed.  Probably 
     * SET_CHANGE_NOTIFICATION_KEY.
     * @param mutationType The type of modification being performed.
     * @param modifyingSet The set of objects performing the mutation.
     */
    protected void willChangeValueForKey(String key, KeyValueObserver.SetMutationType mutationType, Set modifyingSet)
    {
        
    }
    
    /** An empty set, which is used for clear. */
    private static final Set EMPTY_SET = new AbstractSet()
    {
        private final Iterator EMPTY_ITERATOR = new Iterator()
        {
            public boolean hasNext()
            {
                return false;
            }
            public Object next()
            {
                throw new NoSuchElementException("This is the empty set.");
            }
            public void remove()
            {
                throw new IllegalStateException("No element to remove.");
            }
        };
        public Iterator iterator()
        {
            return EMPTY_ITERATOR;
        }
        public int size()
        {
            return 0;
        }
    };
    
    /** Sets that only contain one item. */
    private class SingleSet extends AbstractSet
    {
        private Object item;
        
        public SingleSet(Object item)
        {
            this.item = item;
        }
        
        public Iterator iterator()
        {
            return new Iterator()
            {
                boolean hasNext = true;
                public boolean hasNext()
                {
                    return hasNext;
                }
                public Object next()
                {
                    if (hasNext)
                    {
                        hasNext = false;
                        return item;
                    } else
                        throw new NoSuchElementException("There are no more items in the set.");
                }
                public void remove()
                {
                    throw new UnsupportedOperationException("You can't modify this set.");
                }
            };
        }

        public int size()
        {
            return 1;
        }
    }
}
